Skip to content

test(plugin-security): feed the VAMA DELETE half a call shape the engine accepts (#6277) - #6502

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-6277-vama-delete-dispatch-shape
Aug 8, 2026
Merged

test(plugin-security): feed the VAMA DELETE half a call shape the engine accepts (#6277)#6502
baozhoutao merged 1 commit into
mainfrom
claude/issue-6277-vama-delete-dispatch-shape

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #6277

问题

packages/plugins/plugin-security/src/vama-write-path-convergence.test.tswrite('delete', …) 把一次 by-id delete 构造成 options: { id }。这是一个真实引擎会拒绝两次的调用袋:

  1. ObjectQL.delete 入口先跑 rejectUnknownEngineOptions([P2] A direct engine call silently drops sort/select/skip/populate — declared query contract, zero enforcement #4371)——id 不在 delete 的合法键 context / where / multi + driver passthrough 之内,直接抛错;
  2. 即便该键合法,resolveEngineDeleteDispatch 只认 options.where.id,该形状落 reject(测试替身比真实实现宽松:四个缺陷因此带着绿灯发布——需要一条把替身钉在真实契约上的闸门 #4550)。

后果:SecurityPlugin.extractSingleId 读的是 data.idoptions.where.id,对 options.id 返回 null,于是 #1994 行级前像写门在 delete 半边整段跳过。用例名声称「explain 与它所解释的那次写答案一致」,而写的那一侧只跑了两个门里的一个——plugin-sharing 的 canDelete 之所以还在跑,是因为它的 inferTargetId 恰好也认 options.id(sharing-plugin.ts:922),这个不对称正是这件事藏了这么久的原因。

修法

  1. 形状改成引擎真正 dispatch 的 options: { where: { id } }(byIdDeleteOptions)。
  2. 不只是重新拼写,而是钉住:中间件链的终点从一个布尔量改成 fake engine 自己的写动词,delete() / update() 分别以 assertEngineDeleteDispatch(options) / assertEngineUpdateDispatch(data, options) 开头(测试替身比真实实现宽松:四个缺陷因此带着绿灯发布——需要一条把替身钉在真实契约上的闸门 #4550 / objectql 的 UPDATE dispatch 没有共享判定函数 —— delete 有 resolveEngineDeleteDispatch,update 的同款三分支只是 engine.ts 里的一个内联 throw #5480 的既有纪律,抄 pinned fake 而非手写守卫)。check:engine-double-contract 现在把本文件的两个 slice 都报为 pinned。往回漂回旧形状会在终点大声炸掉,而不是从没跑过的门那里再买一次绿。
  3. 新增一条 [#6277] 用例,把旧形状的拒绝面作为断言提交下来(反向验证不再是一次性手工动作)。
  4. { ok: true } 现在意味着「行真的没了」——DELETE 用例断言 crm_contract 只剩 rec_owned

实测:门确实从「没被问过」变成「被问了」

在本分支上对三个 principal 逐一测量(探针在提交前已删除):

delete 调用形状 extractSingleId computeRlsFilter
options: { id }(旧) null(admin / auditor / member 全部) 从未调用
options: { where: { id } }(新) 'rec_ownerless'(全部) ('delete') 各 1 次

诚实边界:没有任何断言变红,原因要说清楚

派单预期「门首次真正执行后既有断言可能变红,需按 main 现状重钉」。本文件没有一条断言需要重钉,而这不是因为门没执行,是因为门执行后答的是「不适用」:

computeRlsFilter 对本文件三个 permission set 全部返回 null——它们都没有 authored RLS policy,不像真实的 member_default(带通配 owner_only_writes,即 #5492 量到的那个 co-gate)。所以前像门的重读/拒绝分支在本 fixture 里仍未被行使;本 PR 钉住的是门的可达性,不是门的判决

issue 正文引用的「同一个 manager 的 delete 从 ok: true 变成 403 (row-level security)」是在 #5492 的复现装置(带 manager + authored RLS)里量到的,不是本文件的 fixture;本文件三个 principal 都没有 RLS,所以形状修正前后判决一致。这一点已写进 fixture 注释,并指明:当 #5492 配对 PR 让 modifyAllRecords 真正扩到行级写门时,本文件的 DELETE 用例就是那个变化在这里变得 load-bearing 的地方。

反向验证(方向:红,且两道防线各自独立)

byIdDeleteOptions 改回 { id: recordId }:

  • 只留显式前置断言 → AssertionError: expected { kind: 'reject', … } to deeply equal { kind: 'by-id', id: 'rec_ownerless' },附 "message": "Delete requires an ID or options.multi=true";
  • 把该前置断言临时注释掉、只靠引擎终点 → Error: Delete requires an ID or options.multi=true,并且没有write() 的 catch 吞成 403(ENGINE_DISPATCH_REJECTIONS 直接重抛)。

两次都只有 DELETE 一条红,其余 11 条绿——即两道防线互不遮蔽。

验证

pnpm --filter @objectstack/plugin-security test
  Test Files  35 passed (35)
       Tests  769 passed (769)      # 改前 768,新增 1 条 [#6277] 用例

pnpm --filter @objectstack/plugin-security typecheck    # tsc --noEmit,无输出
pnpm --filter @objectstack/plugin-security build        # DTS Build success

node scripts/check-engine-double-contract.mjs
  pinned [delete]  packages/plugins/plugin-security/src/vama-write-path-convergence.test.ts
  pinned [update]  packages/plugins/plugin-security/src/vama-write-path-convergence.test.ts
  check-engine-double-contract: OK — 84 pinned, 133 in the DEBT ledger, 4 exempt.

node scripts/check-nul-bytes.mjs        # OK,6106 files,无裸控制字节
node scripts/check-type-check-coverage.mjs   # OK
node scripts/check-override-consistency.mjs  # OK
npx eslint packages/plugins/plugin-security/src/vama-write-path-convergence.test.ts   # exit 0

关于 package.json 与 changeset

为了 import 生产者自己的 dispatch predicate,给 @objectstack/plugin-security 加了 devDependency @objectstack/metadata-core(该包依赖仅 @objectstack/spec + zod,门禁的 modules 白名单显式接受这个拼写)。无环已实测:turbo run build --filter=@objectstack/plugin-security --dry 无 circular / cyclic 告警——与本仓 ledger 中 audience-anchors.test.ts 条目记录的结论一致。

本 PR test-only,不发布任何用户可见变更,故不写 changeset,挂 skip-changeset 标签。

在飞零相交

git ls-remote --heads origin | grep -iE 'issue-(6277|5491|5492|6428)' 零命中;#5491/#5492 配对批次被 Blocked-by: #6428 阻塞、未起飞,本单先落即满足分诊的串行约束。


Generated by Claude Code

…ine accepts (#6277)

`vama-write-path-convergence.test.ts` built its by-id delete as
`options: { id }` — a bag `ObjectQL.delete` refuses twice over: `id` is
outside delete's legal option keys (#4371), and the dispatch predicate
reads `options.where.id`, so the call is `reject` (#4550). Consequently
`SecurityPlugin.extractSingleId` returned `null` and the #1994 row-level
pre-image write gate never ran on this half; only plugin-sharing's
`canDelete` did, because its `inferTargetId` does accept `options.id`.

Measured over all three principals:

  options: { id }             extractSingleId -> null    computeRlsFilter: 0 calls
  options: { where: { id } }  extractSingleId -> id      computeRlsFilter('delete'): 1 call

The fixture now feeds the canonical shape, and the shape is pinned rather
than merely re-spelled: the middleware chain terminates in the fake
engine's own write verbs, which open with `assertEngineDeleteDispatch` /
`assertEngineUpdateDispatch` (both slices of
check:engine-double-contract now report this file as pinned). A drift
back to the old spelling fails loudly at the terminal instead of buying a
green from gates that never ran.

No assertion changed verdict: `computeRlsFilter` resolves `null` for all
three of this file's permission sets, none of which authors an RLS
policy. The gate is reached, not yet exercised to a verdict — that is
#5492's subject, noted in the fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 2:37am

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@baozhoutao baozhoutao added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m labels Aug 8, 2026 — with Claude
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security.

13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Contributor Author

CI 结果:23 success / 7 skipped / 1 failure,唯一那条红是 Validate Package Dependencies,与本 PR 无关,已单独立单 #6506

证据(不是推断):

所以这是当前 main 上每个 PR 都会撞到的全仓状态。与本 PR 直接相关的 job 全绿:Test Core (1-3/3)TypeScript Type CheckESLintBuild CoreDogfood Regression Gate (1-3/3)Check Changeset(skip-changeset 标签在其重读前落位)。

标签读回:['dependencies', 'tests', 'skip-changeset'] —— Auto Label 后写的 dependencies / tests 与先写的 skip-changeset 共存,无互相覆盖。


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review August 8, 2026 02:49
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 77d9001 Aug 8, 2026
31 of 32 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-6277-vama-delete-dispatch-shape branch August 8, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vama-write-path-convergence.test.ts 的 DELETE 用例喂的是引擎会拒绝的调用形状,安全侧前像门在这一半从未被执行

2 participants